import { Component, type ObjectProps, setAttrs, setCSS } from '@knno/jsx';
import { isDescendant } from '../../tools/utils';
import { DATA } from '../symbol';
import { MARGIN, popupStyle } from './popup.css';

type BesideH = 'left' | 'right';
type BesideV = 'top' | 'bottom';
type Beside = BesideH | BesideV;
type AlignH = 'left' | 'right' | 'center';
type AlingV = 'top' | 'bottom' | 'middle';
type Align = AlignH | AlingV;
export type PopupPosition = `${BesideH}-${AlingV}` | `${BesideV}-${AlignH}`;

type Rect = {
	x: number;
	y: number;
	width: number;
	height: number;
};

type CalcBeside = {
	side: Beside;
	value: number;
};

type CalcAlign = {
	align: Align;
	value: number;
};

function besideLeft(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'left';
	let x = ref.x - rect.width;
	if (x < 0 && ref.x + ref.width + rect.width < window.innerWidth) {
		side = 'right';
		x = ref.x + ref.width;
	}
	return {
		side,
		value: x,
	};
}

function besideRight(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'right';
	let x = ref.x + ref.width;
	if (x + rect.width > window.innerWidth && ref.x - rect.width > 0) {
		side = 'left';
		x = ref.x - rect.width;
	}
	return {
		side,
		value: x,
	};
}

function besideTop(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'top';
	let y = ref.y - rect.height;
	if (y < 0 && ref.y + ref.height + rect.height < window.innerHeight) {
		side = 'bottom';
		y = ref.y + ref.height;
	}
	return {
		side,
		value: y,
	};
}

function besideBottom(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'bottom';
	let y = ref.y + ref.height;
	if (y + rect.height > window.innerHeight && ref.y - rect.height > 0) {
		side = 'top';
		y = ref.y - rect.height;
	}
	return {
		side,
		value: y,
	};
}

function alignLeft(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'left';
	let x = ref.x;
	if (x + rect.width > window.innerWidth && ref.x + ref.width - rect.width > 0) {
		align = 'right';
		x = ref.x + ref.width - rect.width;
	}
	return {
		align,
		value: x,
	};
}

function alignRight(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'right';
	let x = ref.x + ref.width - rect.width;
	if (x < 0 && ref.x + ref.width < window.innerWidth) {
		align = 'left';
		x = ref.x;
	}
	return {
		align,
		value: x,
	};
}

function alignCenter(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'center';
	let x = ref.x + (ref.width - rect.width) / 2;
	if (x < 0 && ref.x + ref.width < window.innerWidth) {
		align = 'left';
		x = ref.x;
	} else if (x + rect.width > window.innerWidth && ref.x + ref.width - rect.width > 0) {
		align = 'right';
		x = ref.x + ref.width - rect.width;
	}
	return {
		align,
		value: x,
	};
}

function alignTop(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'top';
	let y = ref.y;
	if (y + rect.height > window.innerHeight && ref.y + ref.height - rect.height > 0) {
		align = 'bottom';
		y = ref.y + ref.height - rect.height;
	}
	return {
		align,
		value: y,
	};
}

function alignBottom(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'bottom';
	let y = ref.y + ref.height - rect.height;
	if (y < 0 && ref.y + ref.height < window.innerHeight) {
		align = 'top';
		y = ref.y;
	}
	return {
		align,
		value: y,
	};
}

function alignMiddle(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'middle';
	let y = ref.y + (ref.height - rect.height) / 2;
	if (y < 0 && ref.y + ref.height < window.innerHeight) {
		align = 'top';
		y = ref.y;
	} else if (y + rect.height > window.innerHeight && ref.y + ref.height - rect.height > 0) {
		align = 'bottom';
		y = ref.y + ref.height - rect.height;
	}
	return {
		align,
		value: y,
	};
}

export type Pos = { x: number; y: number };

type Focusable = {
	focus: () => void;
};

function isFocusable<T extends Element>(elem: T | undefined): elem is T & Focusable {
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	return (elem as any).focus instanceof Function;
}

export class PopupBox extends Component<ObjectProps<PopupBox>, PopupBox, HTMLDivElement> {
	protected render() {
		return (
			<div
				class={popupStyle}
				onKeyDown={(evt) => {
					if (evt.key === 'Escape') {
						evt.stopPropagation();
						this.close(true);
						if (isFocusable(this[DATA].binding)) {
							this[DATA].binding.focus();
						}
					}
				}}
				onContextMenu={(evt) => {
					evt.preventDefault();
				}}
				onOpen={() => {
					this[DATA].onOpen?.();
				}}
				onClose={(evt) => {
					const isEsc = !!(evt as CustomEvent).detail;
					this[DATA].onClose?.(isEsc);
				}}
			>
				<div class="panel" tabIndex={0}></div>
			</div>
		);
	}

	protected get panel(): HTMLDivElement {
		return this.el.querySelector(':scope>div.panel')!;
	}

	setContent(elem: (Node | string)[] | Node | string): this {
		if (Array.isArray(elem)) {
			this.panel.replaceChildren(...elem);
		} else {
			this.panel.replaceChildren(elem);
		}
		return this;
	}

	addClass(cls: string): this {
		this.el.classList.add(...cls.split(/\s+/g).filter((v) => !!v));
		return this;
	}

	onOpen(cb: (() => void) | undefined): this {
		this[DATA].onOpen = cb;
		return this;
	}

	onClose(cb: ((esc: boolean) => void) | undefined): this {
		this[DATA].onClose = cb;
		return this;
	}

	onWillClose(cb: ((esc: boolean) => void) | undefined): this {
		this[DATA].onWillClose = cb;
		return this;
	}

	private [DATA]: {
		visible: boolean;
		binding?: Element;
		beside?: Beside;
		align?: Align;
		calcSide?: Beside;
		calcAlign?: Align;
		pos?: Pos;
		bubbleStyle?: boolean;
		onOpen?: () => void;
		onClose?: (esc: boolean) => void;
		onWillClose?: (esc: boolean) => void;
	};

	private onFocusChange: () => void;
	constructor(props: ObjectProps<PopupBox>) {
		super(props);
		this[DATA] = { visible: false };
		this.onFocusChange = () => {
			setTimeout(() => {
				if (this.shouldClose()) {
					this.close();
				}
			});
		};
	}

	private shouldClose() {
		if (!document.activeElement) return true;
		if (isDescendant([this.el], document.activeElement)) return false;
		if (this[DATA].binding && isDescendant([this[DATA].binding], document.activeElement))
			return false;
		return true;
	}

	showUp(focus: boolean) {
		if (!this[DATA].visible) {
			this.el.classList.toggle('float', !!this[DATA].pos);
			this[DATA].visible = true;
			let parent: Element = document.body;
			let elem: Element | undefined | null = this[DATA].binding;
			while (elem) {
				if (elem.classList.contains(popupStyle)) {
					parent = elem;
					break;
				} else {
					elem = elem.parentElement;
				}
			}
			parent.append(this.el);
			if (focus) this.el.focus();
			document.documentElement.addEventListener('focusin', this.onFocusChange);
			document.documentElement.addEventListener('mousedown', this.onFocusChange);
			setTimeout(() => {
				this.el.classList.add('active');
				this.el.dispatchEvent(new Event('open'));
			});
			this.position();
		}
	}

	padding(value: number | string | null) {
		this.panel.style.padding = value ? value + '' : '';
		return this;
	}

	noPointerEvents(value: boolean) {
		this.el.classList.toggle('no-pointer-events', value);
		return this;
	}

	noIndicator(value: boolean) {
		if (value) this.el.classList.add('no-indicator');
		return this;
	}

	show(refer: Element, position: PopupPosition | Pos = 'bottom-center', focus = true) {
		this[DATA].binding = refer;
		if (typeof position === 'string') {
			this[DATA].beside = position.split('-')[0] as Beside;
			this[DATA].align = position.split('-')[1] as Align;
			this[DATA].pos = undefined;
		} else {
			this[DATA].pos = position;
			this[DATA].beside = undefined;
			this[DATA].align = undefined;
		}
		this.showUp(focus);
		return this;
	}

	close(esc = false) {
		if (this[DATA].visible) {
			this[DATA].visible = false;
			this[DATA].onWillClose?.(esc);
			this.el.classList.remove('active');
			document.documentElement.removeEventListener('focusin', this.onFocusChange);
			document.documentElement.removeEventListener('mousedown', this.onFocusChange);
			setTimeout(() => {
				this.el.remove();
				this.el.dispatchEvent(new CustomEvent('close', { detail: esc }));
			}, 200);
		}
		return this;
	}

	private calcAlignV(ref: Rect, rect: Rect): CalcAlign {
		if (this[DATA].align === 'top') {
			return alignTop(ref, rect);
		} else if (this[DATA].align === 'bottom') {
			return alignBottom(ref, rect);
		} else {
			return alignMiddle(ref, rect);
		}
	}

	private calcAlignH(ref: Rect, rect: Rect): CalcAlign {
		if (this[DATA].align === 'left') {
			return alignLeft(ref, rect);
		} else if (this[DATA].align === 'right') {
			return alignRight(ref, rect);
		} else {
			return alignCenter(ref, rect);
		}
	}

	private position() {
		if (this[DATA].visible) {
			setCSS(this.el, {
				left: '10px',
				top: '10px',
			});
			const rect: Rect = this.el.getBoundingClientRect();
			if (this[DATA].pos) {
				let { x, y } = this[DATA].pos;
				if (x < MARGIN) x = MARGIN;
				if (x + rect.width > window.innerWidth - MARGIN) {
					if (x - rect.width >= MARGIN && x < window.innerWidth - MARGIN) {
						x -= rect.width;
					} else {
						x = window.innerWidth - MARGIN - rect.width;
					}
				}
				if (y < MARGIN) y = MARGIN;
				if (y + rect.height > window.innerHeight - MARGIN) {
					if (y - rect.height >= MARGIN && y < window.innerHeight - MARGIN) {
						y -= rect.height;
					} else {
						y = window.innerHeight - MARGIN - rect.height;
					}
				}
				setCSS(this.el, {
					left: x + 'px',
					top: y + 'px',
				});
				setAttrs(this.el, {
					side: null,
					align: null,
				});
			} else {
				const ref: Rect = this[DATA].binding
					? this[DATA].binding.getBoundingClientRect()
					: { x: 0, y: 0, width: 0, height: 0 };
				let side: CalcBeside;
				let align: CalcAlign;
				if (this[DATA].beside === 'left') {
					side = besideLeft(ref, rect);
					align = this.calcAlignV(ref, rect);
					setCSS(this.el, {
						left: side.value + 'px',
						top: align.value + 'px',
					});
				} else if (this[DATA].beside === 'right') {
					side = besideRight(ref, rect);
					align = this.calcAlignV(ref, rect);
					setCSS(this.el, {
						left: side.value + 'px',
						top: align.value + 'px',
					});
				} else if (this[DATA].beside === 'top') {
					side = besideTop(ref, rect);
					align = this.calcAlignH(ref, rect);
					setCSS(this.el, {
						left: align.value + 'px',
						top: side.value + 'px',
					});
				} else {
					side = besideBottom(ref, rect);
					align = this.calcAlignH(ref, rect);
					setCSS(this.el, {
						left: align.value + 'px',
						top: side.value + 'px',
					});
				}
				if (this[DATA].calcSide !== side.side || this[DATA].calcAlign !== align.align) {
					this[DATA].calcSide = side.side;
					this[DATA].calcAlign = align.align;
					setAttrs(this.el, {
						side: side.side,
						align: align.align,
					});
				}
			}
			requestAnimationFrame(this.position.bind(this));
		}
	}
}

export const Popup = {
	create(elem: (string | Node)[] | Node | string): PopupBox {
		const pop = new PopupBox({});
		pop.setContent(elem);
		return pop;
	},
};

let popupInfo: { elem: Element; popup: PopupBox } | undefined;

function findTipsElement(target: Element) {
	let elem: Node | null = target;
	while (elem instanceof Element) {
		const message = elem.getAttribute('data-tips');
		if (message) {
			return elem;
		}
		elem = elem.parentNode;
	}
	return undefined;
}

window.addEventListener('mousemove', (e) => {
	let elem: Element | undefined;
	if (!(e.target instanceof Element) || e.buttons !== 0) {
		elem = undefined;
	} else {
		elem = findTipsElement(e.target);
	}
	if (popupInfo) {
		if (popupInfo.elem !== elem) {
			popupInfo.popup.close();
			popupInfo = undefined;
		} else {
			return;
		}
	}
	if (elem) {
		const pop = Popup.create(elem.getAttribute('data-tips') || '');
		pop.noPointerEvents(true);
		pop.show(elem);
		popupInfo = { elem: elem, popup: pop };
	}
});
